const BaseService = require('./BaseService');
const ErrorProxy = require('../exception/BusinessExceptionProxy');
const orderDao = require('../dao/OrderDao');
const tradeDao = require('../dao/TradeDao');
const userDao = require('../dao/UserDao');
const blockAuditDao = require('../dao/BlockAuditDao');
const { OrderType, OrderStatus, TradeStatus, CoinType } = require('../constants/DbEnum');
const depositRate = Number(process.env.depositRate) || 0.05;
const LambdaProxy = require('../utils/LambdaProxy');

/**
 * Order Service
 */
class OrderService extends BaseService {
  /**
   * constructor
   */
  constructor() {
    super(orderDao);
  }

  /**
   * create order
   * @param {Order} order
   * @return {Promise}
   */
  async createOrder(order) {
    if (await blockAuditDao.isBlocking(order.username)) {
      ErrorProxy.blockingUser();
    }

    let user = await userDao.findOne({ username: order.username });

    if (order.orderType == OrderType.limitedBuy) {
      let newBalance = user.get('balance') - order.counterCoinAmount;
      user.set('balance', newBalance.toFixed(4));
    } else if (order.orderType == OrderType.limitedSell) {
      let recentPrice = await tradeDao.getLastPriceTo(order.baseCoinType, order.counterCoinType);
      order.depositPrice = recentPrice * depositRate;
      let newBalance = user.get('balance') - order.depositPrice * order.baseCoinAmount;
      user.set('balance', newBalance.toFixed(4));

      await this._verifyBaseCoinAvailableBalance(order);
    }

    if (user.balance < 0) {
      ErrorProxy.balanceNotEnough(CoinType.USD);
    }

    return this.transaction((t) => {
      let create = this.dao.create(order);
      let update = user.update(user.get());

      return Promise.all([create, update]);
    });
  }

  /**
   * fetch max buy price
   * @param {CoinType} baseCoinType
   * @param {CoinType} counterCoinType
   * @return {Decimal} price
   */
  fetchMaxBuyPrice(baseCoinType, counterCoinType) {
    return this.dao.fetchMaxBuyPrice(baseCoinType, counterCoinType);
  }

  /**
   * fetch min sell price
   * @param {CoinType} baseCoinType
   * @param {CoinType} counterCoinType
   * @return {Decimal} price
   */
  fetchMinSellPrice(baseCoinType, counterCoinType) {
    return this.dao.fetchMinSellPrice(baseCoinType, counterCoinType);
  }

  /**
   * list buy orders with min price
   * @param {Decimal} minPrice
   * @param {CoinType} baseCoinType
   * @param {CoinType} counterCoinType
   * @return {Array<Order>} orders
   */
  listBuyOrdersWithMinPrice(minPrice, baseCoinType, counterCoinType) {
    return this.dao.listBuyOrdersWithMinPrice(minPrice, baseCoinType, counterCoinType);
  }

  /**
   * list sell orders with max price
   * @param {Decimal} maxPrice
   * @param {CoinType} baseCoinType
   * @param {CoinType} counterCoinType
   * @return {Array<Order>} orders
   */
  listSellOrdersWithMaxPrice(maxPrice, baseCoinType, counterCoinType) {
    return this.dao.listSellOrdersWithMaxPrice(maxPrice, baseCoinType, counterCoinType);
  }

  /**
   * list my pending orders
   * @param {String} username
   * @param {Integer} offset default 0
   * @param {Integer} limit default 100
   * @return {Array<Order>} orders
   */
  listMyOrders(username, offset = 0, limit = 100) {
    return this.dao.listMyOrders(username, offset, limit);
  }

  /**
   * list my history orders
   * @param {String} username
   * @param {Integer} offset default 0
   * @param {Integer} limit default 100
   * @return {Array<Order>} orders
   */
  history(username, offset = 0, limit = 100) {
    return this.dao.history(username, offset, limit);
  }

  /**
   * cancel order
   * @param {String} username
   * @param {Integer} orderId
   * @return {Promise}
   */
  async cancelOrder(username, orderId) {
    let order = await this.dao.findOneWithTrades(username, orderId, TradeStatus.INIT);

    if (!order) {
      ErrorProxy.unknownResource();
    }
    if (order.status != OrderStatus.PENDING) {
      ErrorProxy.invalidOrderStatus(order.status);
    }
    const trades = order.get('Trades');
    if (trades.length > 0) {
      ErrorProxy.notAllowCancelOrder(trades.length);
    }

    order.set('status', OrderStatus.CANCELED);

    let user = await userDao.findOne({ username });
    let releaseUSD = 0;
    if (order.orderType == OrderType.limitedBuy) {
      releaseUSD = order.price * order.currentBaseCoinAmount;
    }
    if (order.orderType == OrderType.limitedSell) {
      releaseUSD = order.depositPrice * order.currentBaseCoinAmount;
    }

    return this.transaction((t) => {
      return Promise.all([
        order.update(order.get()),
        user.increment({ balance: releaseUSD.toFixed(4) }),
      ]);
    });
  }

  /**
   * get order with trades
   * @param {String} username
   * @param {Integer} id
   * @return {Order}
   */
  getDetail(username, id) {
    return this.dao.findOneWithTrades(username, id);
  }

  /**
   * get top order book
   * @param {CoinType} baseCoinType
   * @param {CoinType} counterCoinType
   * @param {Integer} top
   * @return {Array<Array<Order>, Array<Order>>}
   */
  topBook(baseCoinType, counterCoinType, top = 5) {
    return Promise.all([
      this.dao.topBook(OrderType.limitedBuy, baseCoinType, counterCoinType, top),
      this.dao.topBook(OrderType.limitedSell, baseCoinType, counterCoinType, top),
    ]);
  }

  /**
   * @param {String} username
   * @param {Array<String>} coinTypes
   * @return {Array}
   */
  getFreezeAmount(username, coinTypes) {
    return this.dao.getFreezeAmount(username, coinTypes);
  }

  /**
   * @param {Order} order
   * @private
   */
  async _verifyBaseCoinAvailableBalance(order) {
    let verified = false;

    const { username, baseCoinType, baseCoinAddress, baseCoinAmount } = order;

    const promises = new Array();
    promises.push(this.getFreezeAmount(username, [baseCoinType]));
    promises.push(LambdaProxy.getCoinBalance(baseCoinAddress, baseCoinType));
    switch (baseCoinType) {
      case CoinType.CETF:
        promises.push(LambdaProxy.getCoinBalance(baseCoinAddress, CoinType.ETH));
    }

    const [freezeAmounts, coinBalance, feeBalance] = await Promise.all(promises);

    console.info('fetched freezeAmounts:', freezeAmounts);
    console.info('fetched coinBalance:', coinBalance);
    console.info('fetched feeBalance:', feeBalance);

    let freezeAmount = 0;
    if (freezeAmounts && freezeAmounts.length > 0) {
      freezeAmount = Number(freezeAmounts[0].amount);
    }

    if (feeBalance) {
      if (Number(feeBalance.balance) < 10 ** 16) {
        ErrorProxy.balanceNotEnough(CoinType.ETH);
      }
    } else {
      coinBalance.balance = Number(coinBalance.balance) - 0.01 * 10 ** coinBalance.decimals;
    }

    if (
      Number(coinBalance.balance) >=
      (baseCoinAmount + freezeAmount) * 10 ** coinBalance.decimals
    ) {
      verified = true;
    }

    if (!verified) {
      ErrorProxy.balanceNotEnough(baseCoinType);
    }
  }
}

module.exports = new OrderService();
